/*
 * Copyright 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.beepmystuff;

import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;

import junit.framework.TestCase;

import org.apache.http.HttpEntity;
import org.apache.http.ProtocolVersion;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHttpResponse;

public class DefaultBmsHttpClientTest extends TestCase {
  private HttpClient mockHttpClient;
  private DefaultBmsHttpClient client;

  @Override
  protected void setUp() throws Exception {
    mockHttpClient = createMock(HttpClient.class);
    client = new DefaultBmsHttpClient(mockHttpClient);
  }

  public void testShouldReturnResponseBody() throws Exception {
    HttpUriRequest dummyRequest = new HttpGet();
    BasicHttpResponse response = createHttpResponse(200);
    String responseBody = "Hello world";
    HttpEntity entity = new StringEntity(responseBody, "UTF-8");
    response.setEntity(entity);
    expect(mockHttpClient.execute(dummyRequest)).andReturn(response);
    replay(mockHttpClient);

    byte[] expected = responseBody.getBytes("UTF-8");
    byte[] actual = client.getResponseBody(dummyRequest);
    assertTrue(Arrays.equals(expected, actual));
    verify(mockHttpClient);
  }

  public void testShouldReturnResponseBodyAsString() throws Exception {
    HttpUriRequest dummyRequest = new HttpGet();
    BasicHttpResponse response = createHttpResponse(200);
    String expected = "Hello world";
    HttpEntity entity = new StringEntity(expected, "UTF-8");
    response.setEntity(entity);
    expect(mockHttpClient.execute(dummyRequest)).andReturn(response);
    replay(mockHttpClient);

    String actual = client.getResponseBodyAsString(dummyRequest);
    assertEquals(expected, actual);
    verify(mockHttpClient);
  }

  public void testShouldFailOnClientProtocolError() throws Exception {
    HttpUriRequest dummyRequest = new HttpGet();
    expect(mockHttpClient.execute(dummyRequest)).andThrow(
        new ClientProtocolException("you did it wrong"));
    replay(mockHttpClient);

    try {
      client.getResponseBody(dummyRequest);
      fail("Expected BmsClientException");
    } catch (BmsClientException e) {
      assertEquals("you did it wrong", e.getMessage());
      assertEquals(ClientProtocolException.class, e.getCause().getClass());
    }
    verify(mockHttpClient);
  }

  public void testShouldFailWithRetryOnInitialIOException() throws Exception {
    HttpUriRequest dummyRequest = new HttpGet();
    expect(mockHttpClient.execute(dummyRequest)).andThrow(
        new IOException("couldn't find server"));
    replay(mockHttpClient);

    try {
      client.getResponseBody(dummyRequest);
      fail("Expected BmsTransportException");
    } catch (BmsTransportException e) {
      assertEquals("couldn't find server", e.getMessage());
      assertEquals(IOException.class, e.getCause().getClass());
    }
    verify(mockHttpClient);
  }

  public void testShouldFailOn4xxResponse() throws Exception {
    HttpUriRequest request = new HttpGet();
    assertFalse(request.isAborted());
    BasicHttpResponse response = createHttpResponse(404);
    expect(mockHttpClient.execute(request)).andReturn(response);
    replay(mockHttpClient);

    try {
      client.getResponseBody(request);
      fail("Expected BmsClientException");
    } catch (BmsClientException e) {
      assertEquals("HTTP request returned HTTP/1.0 404 Otherwise", e.getMessage());
      assertNull(e.getCause());
    }
    assertTrue(request.isAborted());
    verify(mockHttpClient);
  }

  public void testShouldFailWithRetryOn5xxResponse() throws Exception {
    HttpUriRequest request = new HttpGet();
    assertFalse(request.isAborted());
    BasicHttpResponse response = createHttpResponse(503);
    expect(mockHttpClient.execute(request)).andReturn(response);
    replay(mockHttpClient);

    try {
      client.getResponseBody(request);
      fail("Expected BmsTransportException");
    } catch (BmsTransportException e) {
      assertEquals("HTTP request returned HTTP/1.0 503 Otherwise", e.getMessage());
      assertNull(e.getCause());
    }
    assertTrue(request.isAborted());
    verify(mockHttpClient);
  }

  public void testShouldFailOnUnexpectedHttpStatus() throws Exception {
    HttpUriRequest request = new HttpGet();
    assertFalse(request.isAborted());
    BasicHttpResponse response = createHttpResponse(123);
    expect(mockHttpClient.execute(request)).andReturn(response);
    replay(mockHttpClient);

    try {
      client.getResponseBody(request);
      fail("Expected BmsClientException");
    } catch (BmsClientException e) {
      assertEquals("Unexpected HTTP response HTTP/1.0 123 Otherwise", e.getMessage());
      assertNull(e.getCause());
    }
    assertTrue(request.isAborted());
    verify(mockHttpClient);
  }

  public void testShouldFailIfReceive200WithNoBody() throws Exception {
    HttpUriRequest dummyRequest = new HttpGet();
    BasicHttpResponse response = createHttpResponse(200);
    expect(mockHttpClient.execute(dummyRequest)).andReturn(response);
    replay(mockHttpClient);

    try {
      client.getResponseBody(dummyRequest);
      fail("Expected BmsClientException");
    } catch (BmsClientException e) {
      assertEquals("Received 200 OK with no content?!", e.getMessage());
      assertNull(e.getCause());
    }
    verify(mockHttpClient);
  }

  public void testShouldFailWithRetryOnIOExceptionWhileReading() throws Exception {
    HttpUriRequest dummyRequest = new HttpGet();
    BasicHttpResponse response = createHttpResponse(200);
    BasicHttpEntity entity = new BasicHttpEntity();
    entity.setContentLength(2048);

    // Create a mock InputStream that will allow some data to be read, then
    // throws an IOException.
    MockInputStream in = new MockInputStream();

    entity.setContent(in);
    response.setEntity(entity);
    expect(mockHttpClient.execute(dummyRequest)).andReturn(response);
    replay(mockHttpClient);

    try {
      client.getResponseBody(dummyRequest);
      fail("Expected BmsTransportException");
    } catch (BmsTransportException e) {
      assertEquals("Unable to read response from server", e.getMessage());
      assertEquals(IOException.class, e.getCause().getClass());
    }
    assertEquals(3, in.getReads());  // two successful, one failed
    assertTrue(in.isClosed());
    verify(mockHttpClient);
  }

  public void testShouldFailIfResponseDeclaredTooLarge() throws Exception {
    HttpUriRequest request = new HttpGet();
    BasicHttpResponse response = createHttpResponse(200);
    BasicHttpEntity entity = new BasicHttpEntity();
    entity.setContentLength(20 * 1024 * 1024);  // 20MB
    // The dummy InputStream expects no calls.
    entity.setContent(new DummyInputStream());
    response.setEntity(entity);
    expect(mockHttpClient.execute(request)).andReturn(response);
    replay(mockHttpClient);

    try {
      client.getResponseBody(request);
      fail("Expected BmsClientException");
    } catch (BmsClientException e) {
      assertEquals("Response is 20971520 bytes, larger than allowed (10485760 bytes)",
          e.getMessage());
      assertNull(e.getCause());
    }
    assertTrue(request.isAborted());
    verify(mockHttpClient);
  }

  public void testShouldFailIfResponseFoundToBeTooLarge() throws Exception {
    HttpUriRequest request = new HttpGet();
    BasicHttpResponse response = createHttpResponse(200);
    BasicHttpEntity entity = new BasicHttpEntity();
    entity.setContentLength(-1);  // e.g. chunked encoding

    // Allow two 512-byte reads, then throws.
    MockInputStream in = new MockInputStream();

    entity.setContent(in);
    response.setEntity(entity);
    expect(mockHttpClient.execute(request)).andReturn(response);
    replay(mockHttpClient);

    client.setMaxResponseSize(1023);
    try {
      client.getResponseBody(request);
      fail("Expected BmsClientException");
    } catch (BmsClientException e) {
      assertEquals("Read 1024 response bytes, larger than allowed (1023 bytes)",
          e.getMessage());
      assertNull(e.getCause());
    }
    assertTrue(request.isAborted());
    assertEquals(2, in.getReads());  // two successful
    assertTrue(in.isClosed());
    verify(mockHttpClient);
  }

  private BasicHttpResponse createHttpResponse(int status) {
    return new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 0), status,
        (status == 200) ? "OK" : "Otherwise");
  }

  /**
   * A simple handwritten mock for above, because InputStream isn't an
   * interface, and I don't want to start using EasyMock classextension
   * just for this.
   *
   * Allows two 512-byte reads, throws an IOException on the third.
   * Permits exactly one close, but does no explicit validation.
   */
  private static class MockInputStream extends InputStream {
    private int reads = 0;
    private boolean closed = false;

    @Override
    public int read(byte[] b) throws IOException {
      switch (++reads) {
        case 1:
        case 2:
          return 512;
        case 3:
          throw new IOException();
        default:
          throw new AssertionError("unexpected read");
      }
    }

    @Override
    public void close() {
      if (closed) {
        throw new AssertionError("unexpected close");
      }
      closed = true;
    }

    @Override
    public int read() {
      throw new UnsupportedOperationException();
    }

    public int getReads() {
      return reads;
    }

    public boolean isClosed() {
      return closed;
    }
  }

  /**
   * A dummy InputStream that expects no calls.
   */
  private static class DummyInputStream extends InputStream {
    @Override
    public int read() {
      throw new UnsupportedOperationException();
    }
  }
}
